use dashmap::DashMap;
use tower_lsp::lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url};
use tokio::sync::RwLock;
use std::sync::Arc;
use tracing::debug;

/// Represents a text document in the LSP server
#[derive(Debug, Clone)]
pub struct Document {
    pub uri: Url,
    pub language: Option<String>,
    pub version: i32,
    pub content: String,
    pub line_offsets: Vec<usize>,
}

impl Document {
    pub fn new(uri: Url, content: String, language: Option<String>) -> Self {
        let line_offsets = Self::compute_line_offsets(&content);
        Self {
            uri,
            language,
            version: 1,
            content,
            line_offsets,
        }
    }

    pub fn update(&mut self, changes: Vec<TextDocumentContentChangeEvent>) -> Result<(), String> {
        for change in changes {
            match change.range {
                Some(range) => {
                    // Incremental change
                    let start_offset = self.position_to_offset(range.start)?;
                    let end_offset = self.position_to_offset(range.end)?;
                    
                    if start_offset > self.content.len() || end_offset > self.content.len() {
                        return Err("Invalid range for text change".to_string());
                    }
                    
                    self.content.replace_range(start_offset..end_offset, &change.text);
                }
                None => {
                    // Full document change
                    self.content = change.text;
                }
            }
        }
        
        self.version += 1;
        self.line_offsets = Self::compute_line_offsets(&self.content);
        Ok(())
    }

    pub fn position_to_offset(&self, position: Position) -> Result<usize, String> {
        let line = position.line as usize;
        let character = position.character as usize;
        
        if line >= self.line_offsets.len() {
            return Err(format!("Line {} is out of bounds", line));
        }
        
        let line_start = self.line_offsets[line];
        let line_end = if line + 1 < self.line_offsets.len() {
            self.line_offsets[line + 1] - 1 // -1 to exclude the newline
        } else {
            self.content.len()
        };
        
        let line_length = line_end - line_start;
        if character > line_length {
            return Err(format!("Character {} is out of bounds for line {}", character, line));
        }
        
        Ok(line_start + character)
    }

    pub fn offset_to_position(&self, offset: usize) -> Result<Position, String> {
        if offset > self.content.len() {
            return Err(format!("Offset {} is out of bounds", offset));
        }
        
        // Binary search for the line
        let line = match self.line_offsets.binary_search(&offset) {
            Ok(line) => line,
            Err(line) => line.saturating_sub(1),
        };
        
        if line >= self.line_offsets.len() {
            return Err("Invalid line calculation".to_string());
        }
        
        let line_start = self.line_offsets[line];
        let character = offset - line_start;
        
        Ok(Position::new(line as u32, character as u32))
    }

    pub fn get_text_in_range(&self, range: Range) -> Result<String, String> {
        let start_offset = self.position_to_offset(range.start)?;
        let end_offset = self.position_to_offset(range.end)?;
        
        if start_offset > end_offset {
            return Err("Invalid range: start is after end".to_string());
        }
        
        Ok(self.content[start_offset..end_offset].to_string())
    }

    pub fn get_word_at_position(&self, position: Position) -> Result<Option<String>, String> {
        let offset = self.position_to_offset(position)?;
        
        // Find word boundaries
        let start = self.content[..offset]
            .rfind(|c: char| !c.is_alphanumeric() && c != '_')
            .map(|i| i + 1)
            .unwrap_or(0);
            
        let end = self.content[offset..]
            .find(|c: char| !c.is_alphanumeric() && c != '_')
            .map(|i| offset + i)
            .unwrap_or(self.content.len());
        
        if start >= end {
            return Ok(None);
        }
        
        Ok(Some(self.content[start..end].to_string()))
    }

    fn compute_line_offsets(content: &str) -> Vec<usize> {
        let mut offsets = vec![0];
        for (i, ch) in content.char_indices() {
            if ch == '\n' {
                offsets.push(i + 1);
            }
        }
        offsets
    }
}

/// Manages all open documents in the LSP server
pub struct DocumentManager {
    documents: DashMap<Url, Arc<RwLock<Document>>>,
}

impl DocumentManager {
    pub fn new() -> Self {
        Self {
            documents: DashMap::new(),
        }
    }

    pub async fn open_document(&self, uri: Url, content: String, language: Option<String>) {
        debug!("Opening document: {}", uri);
        let document = Document::new(uri.clone(), content, language);
        self.documents.insert(uri, Arc::new(RwLock::new(document)));
    }

    pub async fn update_document(
        &self,
        uri: &Url,
        changes: Vec<TextDocumentContentChangeEvent>,
    ) -> Result<(), String> {
        if let Some(doc_ref) = self.documents.get(uri) {
            let mut document = doc_ref.write().await;
            document.update(changes)?;
            debug!("Updated document: {} (version {})", uri, document.version);
            Ok(())
        } else {
            Err(format!("Document not found: {}", uri))
        }
    }

    pub async fn close_document(&self, uri: &Url) {
        debug!("Closing document: {}", uri);
        self.documents.remove(uri);
    }

    pub fn get_document(&self, uri: &Url) -> Option<Arc<RwLock<Document>>> {
        self.documents.get(uri).map(|entry| entry.value().clone())
    }

    pub async fn get_document_content(&self, uri: &Url) -> Option<String> {
        if let Some(doc_ref) = self.documents.get(uri) {
            Some(doc_ref.read().await.content.clone())
        } else {
            None
        }
    }

    pub async fn get_document_language(&self, uri: &Url) -> Option<String> {
        if let Some(doc_ref) = self.documents.get(uri) {
            doc_ref.read().await.language.clone()
        } else {
            None
        }
    }

    pub fn list_documents(&self) -> Vec<Url> {
        self.documents.iter().map(|entry| entry.key().clone()).collect()
    }

    pub fn document_count(&self) -> usize {
        self.documents.len()
    }
}

impl Default for DocumentManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tower_lsp::lsp_types::Position;

    #[test]
    fn test_position_to_offset() {
        let content = "line 1\nline 2\nline 3";
        let doc = Document::new(
            Url::parse("file:///test.txt").unwrap(),
            content.to_string(),
            None,
        );

        assert_eq!(doc.position_to_offset(Position::new(0, 0)).unwrap(), 0);
        assert_eq!(doc.position_to_offset(Position::new(0, 6)).unwrap(), 6);
        assert_eq!(doc.position_to_offset(Position::new(1, 0)).unwrap(), 7);
        assert_eq!(doc.position_to_offset(Position::new(1, 6)).unwrap(), 13);
        assert_eq!(doc.position_to_offset(Position::new(2, 0)).unwrap(), 14);
    }

    #[test]
    fn test_offset_to_position() {
        let content = "line 1\nline 2\nline 3";
        let doc = Document::new(
            Url::parse("file:///test.txt").unwrap(),
            content.to_string(),
            None,
        );

        assert_eq!(doc.offset_to_position(0).unwrap(), Position::new(0, 0));
        assert_eq!(doc.offset_to_position(6).unwrap(), Position::new(0, 6));
        assert_eq!(doc.offset_to_position(7).unwrap(), Position::new(1, 0));
        assert_eq!(doc.offset_to_position(13).unwrap(), Position::new(1, 6));
        assert_eq!(doc.offset_to_position(14).unwrap(), Position::new(2, 0));
    }

    #[test]
    fn test_get_word_at_position() {
        let content = "function test_function() {";
        let doc = Document::new(
            Url::parse("file:///test.js").unwrap(),
            content.to_string(),
            Some("javascript".to_string()),
        );

        assert_eq!(
            doc.get_word_at_position(Position::new(0, 2)).unwrap(),
            Some("function".to_string())
        );
        assert_eq!(
            doc.get_word_at_position(Position::new(0, 12)).unwrap(),
            Some("test_function".to_string())
        );
    }
}
